﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    static class testExpression
    {
        public static void Test()
        {
            string idStr = GetPropertyName<UserInfo, int>(u => u.Id);
            Console.WriteLine(idStr); // 打印："Id"
            var idPath = GetPath((Expression<Func<UserInfo, int>>)((UserInfo u) => u.Id));
            Console.WriteLine(idPath);

            string nameStr = GetPropertyName<UserInfo, string>(u => u.Name);
            Console.WriteLine(nameStr); // 打印："Name"
            var namePath = GetPath((Expression<Func<UserInfo, string>>)((UserInfo u) => u.Name));
            Console.WriteLine(namePath);

            var memberName = GetMemberName((Expression<Func<UserInfo, string>>)((UserInfo u) => u.Name));
            Console.WriteLine(memberName);
            var pinfo = GetPropertyInfo((Expression<Func<UserInfo, string>>)((UserInfo u) => u.Name));
            Console.WriteLine(pinfo.DeclaringType.FullName + ":" + pinfo.Name);

            // 复杂对象
            Console.WriteLine(GetPropertyName<UserInfo, int>(item => item.DateOfBirth.Year));
            Console.WriteLine(GetPath((Expression<Func<UserInfo, int>>)(item => item.DateOfBirth.Year)));
            Console.WriteLine(GetMemberName((Expression<Func<UserInfo, int>>)(item => item.DateOfBirth.Year)));
            // 通不过
            //Console.WriteLine(GetPropertyInfo<UserInfo, int>(item => item.DateOfBirth.Year));

            var exp = (Expression<Func<UserInfo, int>>)(item => item.DateOfBirth.Year);
            var fun = exp.Compile();
            Console.WriteLine(fun.Invoke(new UserInfo { DateOfBirth = new DateTime(2010, 1, 1) }));

            Expression<Func<int, bool>> f = p => true;
            var a = Expression.And(f.Body, f.Body);
            ParameterExpression pParam = Expression.Parameter(typeof(int), "p");
            var c = (new int[] { 1, 2, 3 }).Where(Expression.Lambda<Func<int, bool>>(a,
                    new ParameterExpression[] { pParam }).Compile());
            foreach (var item in c)
            {
                Console.WriteLine(item);
            }

            //Expression<Func<double, bool>> exp1 = item => item > 2;
            //var exp2 = Expression.Lambda<Func<int, bool>>(exp1.Body, Expression.Parameter(typeof(int))); 通不过
            //var exp2 = Expression.Lambda<Func<int, bool>>(exp1.Body, Expression.Parameter(typeof(int), "item"));         
            //(new int[] { 1, 2, 3 }).Where(exp1.Compile()); 通不过
            //(new int[] { 1, 2, 3 }).Where(exp2.Compile()).ToList().ForEach(item => Console.WriteLine(item));// 通不过  

            Expression<Func<object, bool>> exp1 = item => !string.IsNullOrWhiteSpace((item as string));
            new string[] { "\r\n", "dsffd", "132" }.Where(exp1.Compile()).ToList().ForEach(item => Console.WriteLine(item));

            var str = "1";
            Expression<Func<string, bool>> exp2 = item => item.Contains(str);
            Display("124", exp2); // OK
            Task.Run(() => Display("124", exp2)); // OK
            //str = "24"; // OK
            str = "5"; // Not found 看起来表达式只是维持了对外部变量的引用而已

            int div = 3;
            Expression<Func<int, bool>> exp3 = item => item % div == 0;
            Task.Run(() => Display2(24, exp3));
            //div = 4; // OK
            div = 5; // Failed 奇怪了，似乎还是引用
        }

        public static void Display2(int num, Expression<Func<int, bool>> expression)
        {
            Thread.Sleep(2000);

            if (expression.Compile().Invoke(num))
                Console.WriteLine("Ok");
            else
                Console.WriteLine("Failed");
        }

        public static void Display(string text, Expression<Func<string, bool>> expression)
        {
            Thread.Sleep(1000);

            if (expression.Compile().Invoke(text))
                Console.WriteLine("Ok");
            else
                Console.WriteLine("Not found");
        }

        /// <summary>
        /// 得到一个字段（Field）或属性（Property）名
        /// </summary>
        /// <typeparam name="TProperty"></typeparam>
        /// <param name="express"></param>
        /// <returns></returns>
        public static string GetPropertyName<T, TProperty>(Expression<Func<T, TProperty>> express)
        {
            if (express == null)
            {
                throw new ArgumentNullException("express");
            }
            MemberExpression memberExpression = express.Body as MemberExpression;
            if (memberExpression == null)
            {
                throw new ArgumentException("请为类型 \"" + typeof(T).FullName + "\" 的指定一个字段（Field）或属性（Property）作为 Lambda 的主体（Body）。");
            }
            return memberExpression.Member.Name;
        }

        // more dirty as possible: removing ".Select(x => x." sentence
        private static Regex _removeSelect = new Regex(@"\.Select\s*\(\s*\w+\s*=>\s*\w+\.", RegexOptions.Compiled);
        private static Regex _removeList = new Regex(@"\.get_Item\(\d+\)", RegexOptions.Compiled);
        private static Regex _removeArray = new Regex(@"\[\d+\]", RegexOptions.Compiled);

        /// <summary>
        /// Get Path (better ToString) from an Expression.
        /// Support multi levels: x => x.Customer.Address
        /// Support list levels: x => x.Addresses.Select(z => z.StreetName)
        /// </summary>
        public static string GetPath(this Expression expr)
        {
            // quick and dirty solution to support x.Name.SubName
            // http://stackoverflow.com/questions/671968/retrieving-property-name-from-lambda-expression

            // enum properties seem to get compiled with Convert(prop, Int32) wrapper calls on Mono 5.0+
            // this causes path extraction code below to fail, since a clean "x.y.z" string is expected
            // thus we strip out any Converts found, using a loop in case there are nested Convert expressions
            while (expr.NodeType == ExpressionType.Convert || expr.NodeType == ExpressionType.ConvertChecked)
            {
                expr = ((UnaryExpression)expr).Operand;
            }

            // if is a method call, get first
            while (expr.NodeType == ExpressionType.Lambda)
            {
                if (((LambdaExpression)expr).Body is UnaryExpression unary)
                {
                    expr = unary.Operand;
                }
                else
                {
                    break;
                }
            }

            var str = expr.ToString(); // gives you: "o => o.Whatever"
            var firstDelim = str.IndexOf('.'); // make sure there is a beginning property indicator; the "." in "o.Whatever" -- this may not be necessary?

            var path = firstDelim < 0 ? str : str.Substring(firstDelim + 1).TrimEnd(')');

            path = _removeList.Replace(path, "");
            path = _removeArray.Replace(path, "");
            path = _removeSelect.Replace(path, ".")
                .Replace(")", "");

            return path;
        }

        public static PropertyInfo GetPropertyInfo<TSource, TProperty>(
    Expression<Func<TSource, TProperty>> propertyLambda)
        {
            Type type = typeof(TSource);

            MemberExpression member = propertyLambda.Body as MemberExpression;
            if (member == null)
                throw new ArgumentException(string.Format(
                    "Expression '{0}' refers to a method, not a property.",
                    propertyLambda.ToString()));

            PropertyInfo propInfo = member.Member as PropertyInfo;
            if (propInfo == null)
                throw new ArgumentException(string.Format(
                    "Expression '{0}' refers to a field, not a property.",
                    propertyLambda.ToString()));

            if (type != propInfo.ReflectedType &&
                !type.IsSubclassOf(propInfo.ReflectedType))
                throw new ArgumentException(string.Format(
                    "Expresion '{0}' refers to a property that is not from type {1}.",
                    propertyLambda.ToString(),
                    type));

            return propInfo;
        }

        public static string GetMemberName(this LambdaExpression memberSelector)
        {
            var currentExpression = memberSelector.Body;

            while (true)
            {
                switch (currentExpression.NodeType)
                {
                    case ExpressionType.Parameter:
                        return ((ParameterExpression)currentExpression).Name;
                    case ExpressionType.MemberAccess:
                        return ((MemberExpression)currentExpression).Member.Name;
                    case ExpressionType.Call:
                        return ((MethodCallExpression)currentExpression).Method.Name;
                    case ExpressionType.Convert:
                    case ExpressionType.ConvertChecked:
                        currentExpression = ((UnaryExpression)currentExpression).Operand;
                        break;
                    case ExpressionType.Invoke:
                        currentExpression = ((InvocationExpression)currentExpression).Expression;
                        break;
                    case ExpressionType.ArrayLength:
                        return "Length";
                    default:
                        throw new Exception("not a proper member selector");
                }
            }
        }
    }

    class UserInfo
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public DateTime DateOfBirth { get; set; }
    }
}
